package br.com.afi.web.util.tag;

import java.lang.reflect.Method;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;

import javax.servlet.jsp.JspException;
import javax.servlet.jsp.tagext.BodyTagSupport;

import br.com.afi.util.log.Logger;
import br.com.afi.web.util.HttpScope;
import br.com.afi.web.util.HttpScopeUtil;

/**
 * Através do uso desta tag é possível "invocar" um método de uma classe(className), de um
 * objeto(object) ou de um atributo (attribute) que está armazenado e algum escopo. Se o método
 * possuir atributos os mesmos devem ser informados utilizando a tag {@link ELParameterMethodTag}.<br>
 * Se o método possuir retorno é possível guardar a informação utilizado os atributos var e
 * varScope. Onde var é nome do attributo e o varScope é em que contexto a variável será gravada.
 * Valores possíveis: pageContext,request,session ou servletContext<br>
 * Obs: Caso a classe tenha sido informada, a mesma deve possuir um construtor default público.
 * 
 * @author André de Fontana Ignacio
 * @version 1.0
 */
public class ELInvokeMethodTag extends BodyTagSupport
{
	private static final long serialVersionUID = 1L;
	private static final Logger log = Logger.getLogger( ELInvokeMethodTag.class );
	private List<Object[]> parametersInfo;
	private Class<?> parameterTypes[];
	private Object parameterValues[];

	/**
	 * Nome da classe que possui o método.
	 */
	private String className;

	/**
	 * Objeto que possui o método
	 */
	private Object object;

	/**
	 * Nome do objeto que deve estar em um dos escopos.
	 */
	private String attribute;

	/**
	 * Nome do Método
	 */
	private String name;

	/**
	 * Nome da Variável que receberá o valor do retorno do método
	 */
	private String var;

	/**
	 * Escopo da Varíavel que receberá o valor do retorno do método;
	 */
	private String varScope;

	/**
	 * Construtor
	 */
	public ELInvokeMethodTag()
	{
		super();
	}

	@Override
	public int doStartTag() throws JspException
	{
		parametersInfo = new ArrayList<Object[]>();
		return EVAL_BODY_INCLUDE;
	}

	@Override
	public int doEndTag() throws JspException
	{
		this.buildParametersInfo();
		try
		{
			final HttpScopeUtil scopeUtil = new HttpScopeUtil( this.pageContext );
			Class<?> clazz = null;
			if ( object != null )
			{
				clazz = object.getClass();
			}
			else if ( attribute != null )
			{
				object = scopeUtil.getAttribute( attribute );
				clazz = object.getClass();
			}
			else if ( className != null )
			{
				clazz = Class.forName( className );
				object = clazz.newInstance();
			}
			else
			{
				final JspException ex = new JspException( "Atributo object, objectName ou className obrigatório." );
				log.error( ex.getMessage() );
				throw ex;
			}
			log.debug( "Obtendo o método {0}({1}) da classe {2}.", name, parameterTypes != null ? parameterTypes.length : 0, clazz );
			final Method m = clazz.getMethod( name, parameterTypes );
			log.debug( "Executando o método {0} da classe {1}", m, clazz );
			final Object ret = m.invoke( object, parameterValues );
			if ( var != null && ret != null )
			{
				HttpScope scope = null;
				if ( varScope != null )
				{
					scope = HttpScope.byName( varScope );
				}
				else
				{
					scope = HttpScope.PAGE_CONTEXT;
				}
				scopeUtil.setAttribute( var, ret, scope );
				log.debug( "Retorno do Método {0} gravado no atributo {1} - {2}", name, var, scope.toString() );

			}
		}
		catch ( Exception e )
		{
			final JspException ex = new JspException( MessageFormat.format( "Não foi possível obter e/ou executar o método {0} da classe {1}", name, className ), e );
			log.error( ex.getMessage(), e );
			throw ex;
		}

		return super.doEndTag();
	}

	/**
	 * Adiciona a classe do paramêtro e o seu valor a lista de paramêtros.
	 * 
	 * @param clazz Classe do paramêtro
	 * @param value Valor do Paramêtro
	 */
	protected void addParameter( Class<?> clazz, Object value )
	{
		final Object[] parameterInfo = new Object[2];
		parameterInfo[0] = clazz;
		parameterInfo[1] = value;
		parametersInfo.add( parameterInfo );
	}

	/**
	 * Método responsável pela construção nas informações referentes aos paramêtros
	 * 
	 * @throws JspException Lançada quando não for possível obter estas informações
	 */
	private void buildParametersInfo() throws JspException
	{
		parameterTypes = new Class<?>[parametersInfo.size()];
		parameterValues = new Object[parametersInfo.size()];
		int i = 0;
		for ( Object[] parameterInfo : parametersInfo )
		{
			parameterTypes[i] = ( Class<?> ) parameterInfo[0];
			parameterValues[i] = parameterInfo[1];
			i++;
		}
	}

	public String getClassName()
	{
		return className;
	}

	public void setClassName( String className )
	{
		this.className = className;
	}

	public String getVar()
	{
		return var;
	}

	public void setVar( String var )
	{
		this.var = var;
	}

	public String getVarScope()
	{
		return varScope;
	}

	public void setVarScope( String varScope )
	{
		this.varScope = varScope;
	}

	public String getName()
	{
		return name;
	}

	public void setName( String name )
	{
		this.name = name;
	}

	public Object getObject()
	{
		return object;
	}

	public void setObject( Object object )
	{
		this.object = object;
	}

	public String getObjectName()
	{
		return attribute;
	}

	public void setObjectName( String objectName )
	{
		this.attribute = objectName;
	}

}
